using System;
using System.Windows;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.IsolatedStorage;
using System.Linq;
using System.Runtime.Serialization.Json;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Windows.Browser;

using Matchingo.Common.Constants;
using Matchingo.Common.Types.Types;
using Matchingo.Common.Types.EventArguments;
using Matchingo.Common.Types.Interfaces;
using Matchingo.Services.Facebook.FacebookScoreboard;

namespace Matchingo.Models.FacebookScoreProvider
{
    public class FacebookHighScoreProvider : IFacebookHighScoreProvider, IHighScoreProvider
    {
        #region ========== Private Constants ==========

        private const int LOWEST_GAME_VERSION_SUPPORTED = CommonConstants.GAME_RECORD_VERSION_LOWEST_SUPPORTED;

        #endregion

        #region ========== Public Properties ==========

		// TODO: Create a facebook class or something
		string FBApiKey { get; set; }
		string FBSessionSecret { get; set; }
		string FBSessionKey { get; set; }
		string FBUserId { get; set; }
		string FBAuthToken { get; set; }
		bool IsAuthenticated { get; set; }

		class PendingScoreItem 
		{
			public bool friends { get; set; }
			public int limit { get; set; }  
			public int cc { get; set; } 
		};

		/// <summary>
		/// If the game is waiting for session info, calls are queued and once we get session info they are executed
		/// </summary>
		/// <value>The pending calls.</value>
		Dictionary<Guid, Game> PendingAddScores { get; set; }
		Dictionary<Guid, PendingScoreItem> PendingGetScores { get; set; }

		ScoreServiceClient _fbClient = new ScoreServiceClient();

		private List<HighScoreDTO> _gameRecords;
		private List<HighScoreDTO> GameRecords 
        {
            get { return _gameRecords; }
            set
            {
                _gameRecords = value;
                NotifyPropertyChanged("GameRecords");
            }
        }

		private List<HighScoreDTO> _friendsGameRecords;
		private List<HighScoreDTO> FriendsGameRecords
		{
			get { return _friendsGameRecords; }
			set
			{
				_friendsGameRecords = value;
				NotifyPropertyChanged("GameRecords");
			}
		}
		
		#endregion

        #region ========== Constructors ===========

        public FacebookHighScoreProvider()
        {
			PendingAddScores = new Dictionary<Guid, Game>();
			PendingGetScores = new Dictionary<Guid, PendingScoreItem>();

			GameRecords = new List<HighScoreDTO>();
			FriendsGameRecords = new List<HighScoreDTO>();

			_fbClient.GetSessionInfoCompleted += new EventHandler<Matchingo.Services.Facebook.FacebookScoreboard.GetSessionInfoCompletedEventArgs>(fbClient_GetSessionInfoCompleted);
			_fbClient.AddScoreCompleted += new EventHandler<Matchingo.Services.Facebook.FacebookScoreboard.AddScoreCompletedEventArgs>(fbClient_AddScoreCompleted);
			_fbClient.GetGlobalHighScoresCompleted += new EventHandler<Matchingo.Services.Facebook.FacebookScoreboard.GetGlobalHighScoresCompletedEventArgs>(fbClient_GetGlobalHighScoresCompleted);
			_fbClient.GetFriendsHighScoresCompleted += new EventHandler<Matchingo.Services.Facebook.FacebookScoreboard.GetFriendsHighScoresCompletedEventArgs>(fbClient_GetFriendsHighScoresCompleted);

			string fbapikey, fbsessionsecret, fbsessionkey, fbuserid, fbauthtoken;

			HtmlPage.Document.QueryString.TryGetValue("fb_sig_api_key", out fbapikey);
			HtmlPage.Document.QueryString.TryGetValue("fb_sig_ss", out fbsessionsecret);
			HtmlPage.Document.QueryString.TryGetValue("fb_sig_session_key", out fbsessionkey);
			HtmlPage.Document.QueryString.TryGetValue("fb_sig_user", out fbuserid);
			HtmlPage.Document.QueryString.TryGetValue("auth_token", out fbauthtoken);


			//// for local testing
			//if (String.IsNullOrEmpty(fbapikey) && String.IsNullOrEmpty(fbauthtoken))
			//{
			//    fbapikey = @"a034f68d16e8318e7c0b149202d0499a";
			//    fbsessionkey = @"2.XUItanjJXINY9szgatfJDA__.86400.1248588321-1321525614";
			//    fbsessionsecret = @"VA_yZttEqkFzJ5hNviO1mA__";
			//    fbuserid = "1321525614";
			//}

			if (!String.IsNullOrEmpty(fbauthtoken))
			{
				_fbClient.GetSessionInfoAsync(fbauthtoken);
			}
			else
			{
				FBAuthToken = fbauthtoken;
				FBApiKey = fbapikey;
				FBSessionSecret = fbsessionsecret;
				FBUserId = fbuserid;
				FBSessionKey = fbsessionkey;
				IsAuthenticated = true;
			}
		}

		void fbClient_GetSessionInfoCompleted(object sender, Matchingo.Services.Facebook.FacebookScoreboard.GetSessionInfoCompletedEventArgs e)
		{
			if (e.Result != null)
			{
				FBApiKey = e.Result.apikey;
				FBSessionKey = e.Result.skey;
				FBUserId = e.Result.uid;
				FBSessionSecret = e.Result.ss;
				FBAuthToken = String.Empty;
				IsAuthenticated = true;

				ProcessGetScores();
			}
		}

		void ProcessAddScores()
		{
			foreach(KeyValuePair<Guid, Game> kv in PendingAddScores)
			{
				AddScore(kv.Value);
			}
			PendingGetScores.Clear();
		}

		void ProcessGetScores()
		{
			foreach (KeyValuePair<Guid, PendingScoreItem> kv in PendingGetScores)
			{
				GetHighScoresByCardCountAsync(kv.Value.limit, kv.Value.cc);
			}
			PendingGetScores.Clear();
		}

		#endregion

		#region ========== Public Methods ==========

		public void Initialize()
        {
            
        }

        // NOTE:: we would normally just always save asynchronously, but for some reason the unit tests throw an exception
        // when working with the isolated storage on a background thread.. so unit tests should always save synchronously, not asynch
        public void AddRecord(Game g, bool saveAsynchronously)
        {
            if (g.GameState != Game.GameStateEnum.Ended)
                throw new ArgumentException("FacebookScoreProvider.AddRecord() called with game which has not yet ended");
            if (g.ArtificialGame)
                throw new ArgumentException("FacebookScoreProvider.AddRecord() called to save a record played by an artificial player");

			AddScore(g);

            // if anyone is listening to the RecordAdded event then fire it off
            if (RecordAdded != null)
            {
                RecordAdded(this, new GameRecordAddedEventArgs(g));
            }
        }

		/// <summary>
		/// Takes a Matchingo Game object as the argument, but converts it to our HighScoreDTO and handles
		/// inserting into the high score list depending on whether it has a high enough value
		/// </summary>
		/// <param name="g">The Matchingo Game object</param>
		void AddScore(Game g)
		{
			int scoreCount = (from gr in _friendsGameRecords where gr.cc == (CardCountEnumerationCC)g.CardCount select gr).Count();
			if (scoreCount < 10) // TODO remove hardcoded 10 score assumption
			{
				_gameRecords.Add(ConvertGameToHighScore(g));
			}
			else
			{
				HighScoreDTO lowScore = (from gr in _gameRecords
						where gr.cc == (CardCountEnumerationCC)g.CardCount
						orderby gr.score ascending
						select gr).FirstOrDefault();

				if (g.Score > lowScore.score)
				{
					_gameRecords.Remove(lowScore);
					_gameRecords.Add(ConvertGameToHighScore(g));
				}
			}

			scoreCount = (from gr in _friendsGameRecords 
							where gr.cc == (CardCountEnumerationCC)g.CardCount 
							select gr).Count();
			if (scoreCount < 10)
			{
				_friendsGameRecords.Add(ConvertGameToHighScore(g));
			}
			else
			{
				HighScoreDTO lowScore = (from gr in _friendsGameRecords
						where gr.cc == (CardCountEnumerationCC)g.CardCount
						orderby gr.score ascending
						select gr).FirstOrDefault();

				if (g.Score > lowScore.score)
				{
					_friendsGameRecords.Remove(lowScore);
					_friendsGameRecords.Add(ConvertGameToHighScore(g));
				}
			}

			HighScoreDTO hs = ConvertGameToHighScore(g);
			_fbClient.AddScoreAsync(FBUserId, FBSessionKey, FBSessionSecret, hs);
		}

		void fbClient_AddScoreCompleted(object sender, Matchingo.Services.Facebook.FacebookScoreboard.AddScoreCompletedEventArgs e)
		{			
			
		}

		/// <summary>
		/// Converts a Matchingo Game object into a HighScoreDTO.
		/// </summary>
		/// <param name="g">The Matchingo Game object.</param>
		/// <returns></returns>
		HighScoreDTO ConvertGameToHighScore(Game g)
		{
			HighScoreDTO hs = new HighScoreDTO();
			hs.dn = g.PlayerName;
			hs.uid = FBUserId;
			hs.score = g.Score;
			hs.turns = g.TurnCount;
			hs.tis = (int)Math.Round(g.GameTimeElapsed.TotalSeconds);
			hs.cc = (CardCountEnumerationCC)g.CardCount;
			return hs;
		}

		public List<Game> GetHighScoresByCardCount(int topNCount, int cardCount)
        {
			CardCountEnumerationCC cc = (CardCountEnumerationCC)cardCount;
            // select top XX GameRecords orderedby Score
            var highScores = (from gr in _gameRecords 
							  where gr.cc == cc
                              orderby gr.score descending
                              select gr).Take(topNCount);

            return ConvertHighScoresToGames(highScores.ToList());
        }

		public List<Game> GetFriendsHighScoresByCardCount(int topNCount, int cardCount)
		{
			// select top XX GameRecords orderedby Score
			var highScores = (from gr in _friendsGameRecords 
							  where gr.cc == (CardCountEnumerationCC)cardCount
							  orderby gr.score descending
							  select gr).Take(topNCount);

			return ConvertHighScoresToGames(highScores.ToList());
		}

		/// <summary>
		/// Converts the collection of HighScoresDTOs into Matchingo Game objects
		/// </summary>
		/// <param name="scores">The HighScoreDTO collection to be converted.</param>
		/// <returns></returns>
		List<Game> ConvertHighScoresToGames(IList<HighScoreDTO> scores)
		{
			List<Game> games = new List<Game>();
			foreach (HighScoreDTO hs in scores)
			{
				Game g = ConvertHighScoreToGame(hs);
				games.Add(g);
			}
			return games;
		}

		/// <summary>
		/// Converts a single HighScoreDTO into a Matchingo Game object
		/// </summary>
		/// <param name="hs">The HighScoreDTO to convert.</param>
		/// <returns></returns>
		Game ConvertHighScoreToGame(HighScoreDTO hs)
		{
			Game g = new Game();
			g.FBUserID = hs.uid;
			g.PlayerName = hs.dn;
			g.Score = hs.score;
			g.StartTime = DateTime.Now;
			g.EndTime = g.StartTime + TimeSpan.FromSeconds(hs.tis);
			g.GameState = Game.GameStateEnum.Ended;
			g.CardCount = (int)hs.cc;
			g.TurnCount = hs.turns;
			return g;
		}
		
		public void GetHighScoresByCardCountAsync(int topNCount, int cardCount)
		{
			if (!IsAuthenticated) // if we dont have session info yet, queue the call
			{
				PendingScoreItem item = new PendingScoreItem() { friends = false, limit = topNCount, cc = cardCount };
				PendingGetScores.Add(Guid.NewGuid(), item);
				return;
			}

			_fbClient.GetGlobalHighScoresAsync(FBUserId, FBSessionKey, FBSessionSecret, (CardCountEnumerationCC)cardCount, topNCount);
		}

		public void GetFriendsHighScoresByCardCountAsync(int topNCount, int cardCount)
		{
			if (!IsAuthenticated) // if we dont have session info yet, queue the call
			{
				PendingScoreItem item = new PendingScoreItem() { friends = true, limit = topNCount, cc = cardCount };
				PendingGetScores.Add(Guid.NewGuid(), item);
				return;
			}

			_fbClient.GetFriendsHighScoresAsync(FBUserId, FBSessionKey, FBSessionSecret, (CardCountEnumerationCC)cardCount, topNCount);
		}

		/// <summary>
		/// Deletes high scores from the list that have the specified cardCount
		/// </summary>
		/// <param name="highScores">The high scores.</param>
		/// <param name="cardCount">The card count.</param>
		void ClearHighScores(IList<HighScoreDTO> highScores, CardCountEnumerationCC cardCount)
		{
			IEnumerable<HighScoreDTO> oldScores = (from gr in highScores
						  where gr.cc == cardCount
						  select gr);
			IList<HighScoreDTO> removeThese = oldScores.ToArray();
			foreach (HighScoreDTO score in removeThese)
			{
				highScores.Remove(score);
			}
		}

		void fbClient_GetGlobalHighScoresCompleted(object sender, Matchingo.Services.Facebook.FacebookScoreboard.GetGlobalHighScoresCompletedEventArgs e)
		{
			if (e.Result == null)
				return;

			if (_gameRecords == null)
				_gameRecords = new List<HighScoreDTO>();

			ClearHighScores(_gameRecords, e.Result.CardCount);
			_gameRecords.AddRange(e.Result.HighScores);

			if (GetHighScoresCompleted != null)
			{
				GetHighScoresCompleted(this, new GetHighScoresCompletedEventArgs(ConvertHighScoresToGames(e.Result.HighScores)));
			}
		}

		void fbClient_GetFriendsHighScoresCompleted(object sender, Matchingo.Services.Facebook.FacebookScoreboard.GetFriendsHighScoresCompletedEventArgs e)
		{
			if (e.Result == null)
				return;

			if (_friendsGameRecords == null)
				_friendsGameRecords = new List<HighScoreDTO>();

			ClearHighScores(_friendsGameRecords, e.Result.CardCount);
			_friendsGameRecords.AddRange(e.Result.HighScores);

			if (GetFriendsHighScoresCompleted != null)
			{
				GetFriendsHighScoresCompleted(this, new Matchingo.Common.Types.EventArguments.GetFriendsHighScoresCompletedEventArgs(ConvertHighScoresToGames(e.Result.HighScores)));
			}
		}
		
		#endregion

        #region ========== Private Methods ==========

        private static string SerializeToJsonString(object objectToSerialize)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                DataContractJsonSerializer serializer = new DataContractJsonSerializer(objectToSerialize.GetType());
                serializer.WriteObject(ms, objectToSerialize);
                ms.Position = 0;

                using (StreamReader reader = new StreamReader(ms))
                {
                    return reader.ReadToEnd();
                }
            }
        }

        private static T Deserialize<T>( string jsonString )
        {
            using( MemoryStream ms = new MemoryStream( Encoding.Unicode.GetBytes( jsonString ) ) )
            {
                DataContractJsonSerializer serializer = new DataContractJsonSerializer( typeof( T ) );
                return ( T )serializer.ReadObject( ms );
            }
        }

        private void NotifyPropertyChanged(string propertyName)
        {
            if (PropertyChanged == null)
                return;

            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion

        #region ========== Public Events ==========

        public event PropertyChangedEventHandler PropertyChanged;
		public event EventHandler<Matchingo.Common.Types.EventArguments.GameRecordAddedEventArgs> RecordAdded;
		public event EventHandler<Matchingo.Common.Types.EventArguments.GetHighScoresCompletedEventArgs> GetHighScoresCompleted;
		public event EventHandler<Matchingo.Common.Types.EventArguments.GetFriendsHighScoresCompletedEventArgs> GetFriendsHighScoresCompleted;

        #endregion
    }
}
